Skip to content

fix(workflows): guard non-mapping 'workflow:' block in WorkflowDefinition#3694

Open
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/engine-workflow-block-non-mapping-guard
Open

fix(workflows): guard non-mapping 'workflow:' block in WorkflowDefinition#3694
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/engine-workflow-block-non-mapping-guard

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

What

A present-but-non-mapping top-level workflow: block crashed WorkflowDefinition.__init__ with AttributeError:

  • bare workflow: → YAML null
  • workflow: some-stringstr
  • workflow: [a, b]list

workflow = data.get("workflow", {}) only substitutes {} when the key is absent, so a present non-dict value reached workflow.get("id", "")AttributeError: 'NoneType'/'str'/'list' object has no attribute 'get'.

This fires inside from_yaml/from_stringbefore validate_workflow can report the malformed shape — and in the CLI (workflow run/resume, whose load_workflow is wrapped to catch only FileNotFoundError/ValueError) it escapes as a raw traceback instead of a clean "Invalid workflow" message.

Fix

Normalize the local workflow to {} when it isn't a mapping (self.data keeps the raw value so validate_workflow still reports it), mirroring the adjacent default_options guard (if not isinstance(...): = {}).

Tests

tests/test_workflows.py::TestWorkflowDefinition::test_non_mapping_workflow_block_parses_then_validates (parametrized null/str/list) — construction no longer raises, and validate_workflow reports the missing workflow.id. Fails before the fix (all three crash). ruff clean.


AI-assisted: authored with Claude Code. Verified against the default_options sibling guard and confirmed fail-before/pass-after, incl. self.data round-trip.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents malformed workflow: blocks from crashing workflow parsing.

Changes:

  • Normalizes non-mapping workflow headers locally.
  • Adds regression coverage for null, string, and list values.
Show a summary per file
File Description
src/specify_cli/workflows/engine.py Guards workflow header access.
tests/test_workflows.py Tests malformed header handling.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread tests/test_workflows.py Outdated
@mnriem

mnriem commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

@jawwad-ali
jawwad-ali force-pushed the fix/engine-workflow-block-non-mapping-guard branch from d347029 to 81abc3c Compare July 24, 2026 08:28
@mnriem
mnriem requested a review from Copilot July 24, 2026 15:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment thread src/specify_cli/workflows/engine.py Outdated

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address Copilot feedback

jawwad-ali and others added 2 commits July 25, 2026 00:05
…tion

A present-but-non-mapping top-level `workflow:` block (bare `workflow:` ->
YAML null, or `workflow: <str>` / `workflow: [..]`) crashed
WorkflowDefinition.__init__ with AttributeError: the `{}` default of
`data.get("workflow", {})` only applies when the key is ABSENT, so a non-dict
value reached `workflow.get("id", ...)`. This fires inside from_yaml/
from_string — before validate_workflow can report the malformed shape — and
in the CLI escapes as a raw traceback (load_workflow is wrapped to catch only
FileNotFoundError/ValueError).

Normalize the local `workflow` to {} when it is not a mapping (self.data keeps
the raw value so validate_workflow still reports it), mirroring the adjacent
default_options guard.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…low value

Address review: the previous assertion only proved the key stayed present; it
would pass even if construction replaced the malformed value with {}. Assert
definition.data["workflow"] equals the original parsed value and is still a
non-mapping, proving the guard normalizes only the local variable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jawwad-ali
jawwad-ali force-pushed the fix/engine-workflow-block-non-mapping-guard branch from 81abc3c to 4b26886 Compare July 24, 2026 19:10
@jawwad-ali

Copy link
Copy Markdown
Contributor Author

Correct — fixed in 4b26886. I verified validate_workflow never reads .data (it reads the parsed attributes definition.id/.name/.version/.schema_version); the only reader of .data is the yaml.safe_dump serialization path. The comment no longer claims a validation dependency:

Normalize the local to {} instead: the header fields fall back to their defaults and validate_workflow (which reads those parsed attributes) reports the missing workflow.id/workflow.name. self.data is deliberately left holding the raw value, since it is what gets written back out when a definition is serialized.

Also rebased onto latest main to clear the conflict with the now-merged #3696 (both tests coexist; 5 pass).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants